package com.wang.gateway.filter;


import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.wang.common.core.domain.LoginUser;
import com.wang.common.core.utils.JWTUtil;
import com.wang.common.core.constants.CacheConstants;
import com.wang.common.core.constants.HttpConstants;
import com.wang.common.core.domain.R;
import com.wang.common.core.enums.ResultCode;
import com.wang.common.core.enums.UserIdentity;
import com.wang.common.redis.service.RedisService;
import com.wang.gateway.properties.IgnoreWhiteProperties;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * 网关鉴权
 *
 */
@Slf4j
@Component
//定义全局过滤器
public class AuthFilter implements GlobalFilter, Ordered {

    //排除过滤的uri白名单地址
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Value("${jwt.secret}")
    private String secret; //拿到签名密钥

    @Autowired
    private RedisService redisService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String url = request.getURI().getPath();

        //跳过白名单中的路径 --> 白名单中的路径不需要进行验证
        if (matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }

        //从http请求头中获取token
        String token = getToken(request);
        if (StrUtil.isEmpty(token)) {
            //如果token为空
            return unauthorizedResponse(exchange, "令牌不能为空");
        }

        Claims claims; //Payload中的信息

        try {
            //将Payload的中的信息解析出来
            claims = JWTUtil.parseToken(token, secret);

            if (claims == null) {
                //如果Payload为空
                return unauthorizedResponse(exchange, "令牌已过期或验证不正确！");
            }
        } catch (Exception e) {
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确！");
        }

        String userId = JWTUtil.getUserId(claims); //获取jwt中的userId

        if (StrUtil.isEmpty(userId)) { //判断userId是否存在
            return unauthorizedResponse(exchange, "令牌验证失败");
        }

        boolean isLogin = redisService.hasKey(getTokenKey(userId)); //通过key判断Redis中是否还存在当前用户的信息
        if (!isLogin) {
            //如果不存在:
            return unauthorizedResponse(exchange, "登录状态已过期");
        }

        //把该用户的身份信息从Redis中拿出来
        LoginUser user = redisService.getCacheObject(getTokenKey(userId),LoginUser.class);

        if (url.contains(HttpConstants.SYSTEM_URL_PREFIX) &&
                !UserIdentity.ADMIN.getValue().equals(user.getIdentity())) {
            //如果获取URL中的前缀发现是system前缀,但是当前用户的身份不是管理员
            return unauthorizedResponse(exchange, "令牌验证失败");
        }
        if (url.contains(HttpConstants.FRIEND_URL_PREFIX) &&
                !UserIdentity.ORDINARY.getValue().equals(user.getIdentity())) {
            //从URL中获取前缀发现是friend,但是当前用户不是普通用户
            return unauthorizedResponse(exchange, "令牌验证失败");
        }

        return chain.filter(exchange);
    }



    /**
     * 查找 指定url 是否匹配 指定匹配规则列表 中的任意一个字符串
     *
     * @param url         指定url
     * @param patternList 需要检查的匹配规则列表
     * @return 是否匹配
     */
    private boolean matches(String url, List<String> patternList) {
        if (StrUtil.isEmpty(url) || patternList.isEmpty()) {
            //如果指定url为空 或 匹配规则列表为空
            return false;
        }
        for (String pattern : patternList) {
            //逐一遍历检查
            if (isMatch(pattern, url)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断url是否与规则匹配
     * 匹配规则中：
     * ? 表⽰单个字符;
     * * 表⽰⼀层路径内的任意字符串，不可跨层级;
     * ** 表⽰任意层路径;
     *
     * @param pattern 匹配规则
     * @param url     需要匹配的url
     * @return 是否匹配
     */
    private boolean isMatch(String pattern, String url) {
        AntPathMatcher matcher = new AntPathMatcher();
        return matcher.match(pattern, url);
    }

    /**
     * 获取缓存key
     */
    private String getTokenKey(String userId) {
        return CacheConstants.LOGIN_TOKEN_KEY + userId;
    }

    /**
     * 从请求头中获取请求token
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(HttpConstants.AUTHENTICATION);

        //如果前端设置了令牌前缀，则裁剪掉前缀
        if (StrUtil.isNotEmpty(token) && token.startsWith(HttpConstants.PREFIX)) {
            //如果token不为空 且 token有前缀
            //去除前缀
            token = token.replaceFirst(HttpConstants.PREFIX, StrUtil.EMPTY);
        }
        return token;
    }

    /**
     *
     * 当请求未通过认证(如令牌无效、过期或权限不足)时:
     * 记录错误日志(同时记录请求路径)
     * 构造标准的JSON错误响应, 如 {"code":40101,"msg":"登录状态已过期"}
     * 通过 ServerHttpResponse 将响应返回给客户端
     *
     * @param exchange ServerWebExchange对象,封装了请求和上下文信息.
     * @param msg 信息
     *
     * @return 错误响应
     */
    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {

        log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
        return webFluxResponseWriter(exchange.getResponse(), msg,
                ResultCode.FAILED_UNAUTHORIZED.getCode());
    }

    //拼装webflux模型响应
    private Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String msg, int code) {
        response.setStatusCode(HttpStatus.OK); //设置http响应的状态码
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE,
                MediaType.APPLICATION_JSON_VALUE); //设置header
        R<?> result = R.fail(code, msg);
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
        return response.writeWith(Mono.just(dataBuffer)); //异步返回result
    }

    @Override
    public int getOrder() {
        return -200;
    }
}